Skip to content

feat: delete tag#3025

Closed
jesperhodge wants to merge 2 commits intoopenedx:masterfrom
jesperhodge:feat--delete-without-modal
Closed

feat: delete tag#3025
jesperhodge wants to merge 2 commits intoopenedx:masterfrom
jesperhodge:feat--delete-without-modal

Conversation

@jesperhodge
Copy link
Copy Markdown
Contributor

@jesperhodge jesperhodge commented Apr 24, 2026

Description

This is part of openedx/modular-learning#260 - a split-out so that it's smaller PRs to review.

The intention is to implement the actual deletion logic here, but not implement the Delete Modal yet.
The Delete Modal is replaced by a simple browser confirm for now, the PR to implement that is #3024.

To deliver the Delete functionality cleanly, I have included quite a bit of refactoring, which includes adding a React context.

Both PRs are designed to be independent, mergable in any order.

Important

After merging the other PR, the browser confirm should be replaced with its <DeleteModal>.

Note to reviewer

If there's missing test coverage, please allow me to delay addressing that until it the PR undergone a first review.

Testing instructions

Please see AC of the related ticket for exact behavior to test: openedx/modular-learning#260.

But: note that anything related to the delete modal is in the sister PR.

Use of AI

  • AI helped write the tests
  • AI split apart the code from the delete functionality to have two independent PRs

Best Practices Checklist

We're trying to move away from some deprecated patterns in this codebase. Please
check if your PR meets these recommendations before asking for a review:

  • Any new files are using TypeScript (.ts, .tsx).
  • Avoid propTypes and defaultProps in any new or modified code.
  • Tests should use the helpers in src/testUtils.tsx (specifically initializeMocks)
  • Do not add new fields to the Redux state/store. Use React Context to share state among multiple components.
  • Use React Query to load data from REST APIs. See any apiHooks.ts in this repo for examples.
  • All new i18n messages in messages.ts files have a description for translators to use.
  • Avoid using ../ in import paths. To import from parent folders, use @src, e.g. import { initializeMocks } from '@src/testUtils'; instead of from '../../../../testUtils'

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Apr 24, 2026
@openedx-webhooks
Copy link
Copy Markdown

Thanks for the pull request, @jesperhodge!

This repository is currently maintained by @bradenmacdonald.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 2026

Codecov Report

❌ Patch coverage is 90.83969% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.44%. Comparing base (57b770b) to head (e3b446b).
⚠️ Report is 14 commits behind head on master.

Files with missing lines Patch % Lines
src/taxonomy/tree-table/TreeTableContext.tsx 16.66% 10 Missing ⚠️
src/taxonomy/tree-table/SaveErrorAlert.tsx 75.00% 1 Missing ⚠️
src/taxonomy/tree-table/TableBody.tsx 85.71% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3025      +/-   ##
==========================================
- Coverage   95.47%   95.44%   -0.04%     
==========================================
  Files        1383     1383              
  Lines       32645    32631      -14     
  Branches     7472     7454      -18     
==========================================
- Hits        31168    31144      -24     
- Misses       1408     1418      +10     
  Partials       69       69              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mgwozdz-unicon mgwozdz-unicon mentioned this pull request Apr 26, 2026
7 tasks
Copy link
Copy Markdown
Contributor

@mgwozdz-unicon mgwozdz-unicon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to my comments that I think should be addressed, I have a note for the future: I think this PR contains too much preemptive refactoring outside the scope of the Github Issue requested functionality given that we're trying to preserve hours (pulling Actions.tsx out of tagColumns.tsx into its own file, renaming Tag to Row, and addressing the TODO to introduce a React context instead of passing props). I think these were all good changes that improved the quality of the code, and I like that. However, given that we're trying to preserve hours, I think this went beyond the scope of the ticket (unless the other PR reviewers disagree, in which case I defer to their preferences). Additionally, the preemptive refactoring also increases the PR size. imo, refactors are a better candidate for splitting PRs than attempting to split functionality. Also, while we're trying to preserve hours, I know our reviewers are tight on time as well with the code cut coming up and may want to be able to approve the functionality for the release and save the refactor to review when they have more time available. Given that the investment is already in here for refactoring, I don't think it should be pulled out at this time, just noting for the future.

Additionally, I believe you mentioned this before but I just don't want to lose sight of it: Do we need a separate backend PR for the Meilisearch again to ensure that the deletion really does apply across all tagged content and Libraries?

Upon testing locally, there are some interesting things I noted, but I think they might be expected behavior and/or unrelated to this ticket, but I'll call them out just in case:

  1. When I delete a tag that was associated to a piece of content and then I return to the Course Outline page for that content, the tag counts still include the prior deleted tag in the count but if you view the tag count, the deleted tag is gone and once you refresh the page, the tag counts are updated.
  2. When I delete a tag that was associated to a piece of Library content and then I return to the Library, I see the same thing: Tag count is unchanged. However, also when I click on the piece of content, the tag also still appeared to exist until I did a hard refresh (Cmd+Shift+R) on the page. After the hard refresh, the count is still not updated, but when I click on the content, the tag is gone. As expected, the Meilisearch is not updated.

import { Row } from '@tanstack/react-table';

const DELETE_CONFIRM_MESSAGE =
'Warning: are you sure you want to delete this tag and all its subtags and descendants? Any tags applied to course content will be deleted.';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering why this wasn't internationalized and then I remembered that it's because this functionality isn't intended to be here long term. These aspects should probably be annotated with the TODO comments like you had in #3024 assuming that the decision is to continue having these as separate PRs. Alternatively, I think maybe this PR should have been in a Draft state until testing artifacts can be removed.

axiosMock.onGet(subTagsUrl).reply(200, subTagsResponse);
cleanup();
({ axiosMock } = initializeMocks({ user: adminUser }));
axiosMock
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these mocks initialized twice? Is one set redundant?

});
});

it('does not issue a delete request when browser confirmation is canceled and leaves the table unchanged', async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was expecting this test to have something about clicking a cancel button in it. I think it's missing because it's using the browser confirmation instead of the modal. Just noting that this will probably ultimately need to be updated.

const [tagTree, setTagTree] = useState<TagTree | null>(null);
const [isCreatingTopTag, setIsCreatingTopTag] = useState(false);
const [activeActionMenuRowId, setActiveActionMenuRowId] = useState<RowId | null>(null);
const [, setActiveActionMenuRowId] = useState<RowId | null>(null);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove dangling comma.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct JS syntax. The array has two items, I'm grabbing the second one. The first one is not saved as an unused variable - which the linter forbids - but instead omitted. Without the comma, the code will be incorrect.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, looking at it closer, maybe I'm used to seeing this syntax as const [_, setActiveActionMenuRowId] = useState<RowId | null>(null); to clarify, but it seems like either way is fine.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently my suggestion fails the validation tests, so definitely nevermind on this.

draftError: string | undefined;
isError: boolean | undefined;
isUpdateError: boolean | undefined;
isAdditionalError?: boolean;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this variable name to be a little confusing. What is an "AdditionalError"? I think isError corresponds to the error on create and the isAdditionalError is corresponding to the error on delete, but if that's the case then I think the variable names should be updated to be more explicit about what they represent. Alternatively, maybe they could be consolidated to something like hasMutationError that works for all of them?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree isAdditionalError isn't a very clear name here. If we're specifically using it for delete errors then isDeleteError could work. I do think that a future cleanup could be to replace the multiple error type flags with a single isError var that can be set to different error types, but I'd prefer that be a follow-up and not part of this PR.

Copy link
Copy Markdown
Contributor

@brian-smith-tcril brian-smith-tcril left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I think this is looking good. I left a few comments with questions.

I also agree with Mary that adding a refactor to a new feature PR isn't ideal (especially this close to code cut). More lines to review means more chances for bugs to slip through the review process, and the added time pressure of getting the review done in time for the cut makes that even more likely.

That isn't to say I don't like the refactor, it just would have been nice to have a small PR following existing patterns for the cut, and a refactor as a fast-follow after the cut.

I don't think this PR should be reworked to not include the refactor (we've already started the review process), just a thing to keep in mind for the future.

draftError: string | undefined;
isError: boolean | undefined;
isUpdateError: boolean | undefined;
isAdditionalError?: boolean;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree isAdditionalError isn't a very clear name here. If we're specifically using it for delete errors then isDeleteError could work. I do think that a future cleanup could be to replace the multiple error type flags with a single isError var that can be set to different error types, but I'd prefer that be a follow-up and not part of this PR.

Comment on lines +298 to +311
try {
// In view mode, the table reloads on change, reflecting the deletion
// without needing to manually update the table state
enterViewMode();
await deleteTagMutation.mutateAsync({ value: rowData.value, withSubtags: shouldDeleteSubtags });
setToast({
show: true,
message: intl.formatMessage(messages.tagsDeleteSuccessMessage, { count }),
});
} catch (error) {
const errorMessage = getErrorMessage(error);
setDraftError(errorMessage);
setToast({ show: true, message: intl.formatMessage(messages.tagDeleteErrorMessage, { errorMessage }) });
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude flagged this as a possible race condition, I asked Claude to elaborate and got this response:

enterViewMode(); // 1
await deleteTagMutation.mutateAsync({ ... }); // 2
setToast({ ... }); // 3

The intent (per the comment) is that view mode auto-reloads from the server when data changes, so after the delete succeeds and queries are invalidated, the table refreshes automatically without manually removing the row from the tree.

On success this works fine. The chain is: enter view mode → delete succeeds → onSuccess invalidates queries → refetch → useEffect rebuilds tree → deleted tag gone.

On failure it gets murkier. The catch block does:
enterViewMode(); // already happened
// ...mutation fails...
setDraftError(errorMessage);
setToast({ ... });

The SaveErrorAlert needs both isAdditionalError (from deleteTagMutation.isError) and draftError to be truthy — so the error should render. But the mode transition already happened before the failure, so the table is sitting in view mode looking completely normal while the request is in flight. There's no loading/pending state for the delete — the user gets no feedback that anything is happening, then an error alert appears seemingly out of nowhere.

The bigger concern is that nothing disables the UI during the delete. hasOpenDraft is false (we're in view mode, not draft mode), so all the action menu buttons remain enabled. The user could start an edit, create, or trigger another delete while the first delete is still in flight. If both complete, you get interleaved state updates.

So "race condition" was probably too strong — it's more of a missing in-progress state. The question for the PR author is: should there be some guard (even a simple deleteTagMutation.isPending check) that prevents other actions while a delete is in flight?

Comment on lines +294 to +296
// If the tag in the frontend state does not have subtags,
// don't allow the backend to delete subtags.
// That prevents problems in case of stale frontend state.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate on this a bit? If the frontend shows a tag without any subtags, but the backend has subtags for that tag, and then this runs, could that result in orphaned subtags on the backend?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the backend guards against that case here: https://github.com/openedx/openedx-core/blob/6777915a956795ffd421baaeb9fc49ce50cad696/src/openedx_tagging/models/base.py#L691

I'll update the code comment to this for clarity:

    // Only request recursive deletion when the frontend has loaded descendants.
    // If this state is stale and the backend finds subtags while with_subtags is false,
    // the backend rejects the request instead of deleting the parent alone.

mgwozdz-unicon added a commit to mgwozdz-unicon/frontend-app-authoring that referenced this pull request Apr 28, 2026
@mphilbrick211 mphilbrick211 moved this from Needs Triage to In Eng Review in Contributions Apr 29, 2026
@ormsbee
Copy link
Copy Markdown
Contributor

ormsbee commented Apr 29, 2026

Superseded by #3035

@ormsbee ormsbee closed this Apr 29, 2026
@github-project-automation github-project-automation Bot moved this from In Eng Review to Done in Contributions Apr 29, 2026
@brian-smith-tcril brian-smith-tcril mentioned this pull request Apr 29, 2026
7 tasks
mgwozdz-unicon added a commit to mgwozdz-unicon/frontend-app-authoring that referenced this pull request Apr 29, 2026
brian-smith-tcril pushed a commit that referenced this pull request Apr 29, 2026
* feat: delete tag

* fix: lint

* fix: cleanup from comments on PR #3025

* fix: implement disableTagActions logic

* fix: remove unused variable

* fix: fix unsafe optional chaining lint

* feat: Add delete modal to delete functionality

* fix: code coverage

* fix: lint formatting

---------

Co-authored-by: Jesper Hodge <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

open-source-contribution PR author is not from Axim or 2U

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

6 participants